package neuralNetwork;

import java.text.DecimalFormat;



public class CTRNNNeuron {

	private static int nofNeurons = 0;
	private int neuronId; 
	
	
//	private int[] neighbours; 
	private double[] weights; 
	private CTRNNLayer nextLayer;
	private CTRNNLayer thisLayer; 
//	private CTRNN ctrnn; 
	private double input; 
	private double activation;
	private double gain = 1;
	private double timeConstant = 1;
	private double bias = 0;
	protected DecimalFormat decFormat = new DecimalFormat("0.000");

	
	public CTRNNNeuron(CTRNNLayer thisLayer, CTRNNLayer nextLayer){
		neuronId = nofNeurons++; 
//		this.neighbours = neighbours;
//		this.ctrnn = ctrnn; 
		if (nextLayer == null){
			weights = new double[thisLayer.nofNeurons()]; 
		} else if(thisLayer == null){
			weights = new double[nextLayer.nofNeurons()]; 
		} else {
			weights = new double[nextLayer.nofNeurons() + thisLayer.nofNeurons()]; 
		}
		this.nextLayer = nextLayer; 
		this.thisLayer = thisLayer; 
		input = 0; 
	}
	
	public int getNeuronId(){
		return neuronId; 
	}

	public int nofWeights() {
		return weights.length;
	}

	public double[] getWeights() {
		return weights;
	}

	public void setWeights(double gain, double timeConstant, double bias, double[] weights) {
		if (weights.length != this.weights.length) {
			throw new IllegalArgumentException("The length of the weights are wrong"); 
		}
		this.weights = weights; 
		this.gain = gain;
		this.timeConstant = timeConstant;
		this.bias = bias; 
	}
	
	public void updateInput(double update){
		input += update; 
	}
	
	public void fireUpdate(){
//		System.out.println(this);
		setActivatio(getActivation() + getDeltaActivation());
		if (nextLayer != null){
			double output = getOutput();
//			System.out.println("Neuron id: " + neuronId + ", output: " + output);
			for (int i = 0; i < nextLayer.nofNeurons(); i++){
				CTRNNNeuron nextNeuron = null;
				nextNeuron = nextLayer.get(i); 
				nextNeuron.updateInput(output * weights[i]);
			}
		}
		resetNeuron(); 
//		System.out.println(this);
//		System.out.println();
	}
	
	public void fireLayerUpdate(){
		if (thisLayer == null) {
			return; 
		}
		double output = getOutput();
		int weightShift = 0; 
		if (nextLayer != null) {
			weightShift = nextLayer.nofNeurons(); 
		}
		for (int i = 0; i < thisLayer.nofNeurons(); i++){
			CTRNNNeuron nextNeuron = null;
//			System.out.println(weights.length);
//			System.out.println("weightshift is " + weightShift + " and i is " + i);
			nextNeuron = thisLayer.get(i); 
			nextNeuron.updateInput(output*weights[i+weightShift]);
		}
	}
	
	private void resetNeuron() {
		input = 0; 
	}

	public void setInputSensor(boolean sensor) {
		this.activation = sensor ? Double.MAX_VALUE : -Double.MAX_VALUE; 
	}
	
	public double returnOutput(){
		double output = getOutput();
		resetNeuron();
		return output;
	}
	
	private double getOutput(){
		double output = OutputFunctions.logisticFunction(getActivation()*getGain());
//		System.out.println("Output. " + output);
		if (output != Double.NaN) {
			return output; 
		} else {
			throw new IllegalStateException("The output is NaN"); 
		}
	}

	
	private double getDeltaActivation(){
		if (Math.abs(activation) == Double.MAX_VALUE) {
			return 0;
		}
		double deltaActivation = (-getActivation() + getInput() + getBias())/getTimeConstant();
//		System.out.println("Delta activation: " + deltaActivation);
		if (deltaActivation != Double.NaN) {
			return (deltaActivation);
		} else {
			return 0; 
		}
	}

	private double getGain(){
		return gain; 
	}
	
	private double getActivation() {
		return activation;
	}
	
	private void setActivatio(double activation){
		this.activation = activation; 
	}

	private double getInput(){
		return input;
	}
	
	private double getBias() {
		return bias;
	}

	private double getTimeConstant() {
		return timeConstant;
	}
	
	public String toString(){
		String theString = ""; 
		theString += "Gain: " + decFormat.format(gain); 
		theString += ", TimeConstant: " + decFormat.format(timeConstant); 
		theString += ", Bias: " + decFormat.format(bias); 
		theString += ", Weights: ";
		for (int i = 0; i < weights.length; i++) {
			theString += decFormat.format(weights[i]) + " ";
		}
		return theString; 
	}
	
	private static class OutputFunctions{
		
		public static double logisticFunction(double input){
			return 1/(1+Math.exp(-input));  
		}
		
	}
	
//	public void resetActivation(){
//		activation = 0;
//		input = 0;
//	}

	public String activationString() {
		return decFormat.format(getActivation());
	}
	
	
}
